package birdsong.server;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;

import birdsong.comm.ServerComm;
import birdsong.comm.ServerCommunicator;
import birdsong.comm.ServerSideMessage;


public class Server implements BirdsongServer{
        /**
         * LinkedList of all the connected users (Clients) 
         */
        private LinkedList<Client> onlineClientsList = new LinkedList<Client>();
        private ServerComm serverComm;

        // STANDARD ERROR MESSAGES
        public static final String ERROR_CLIENT_OFFLINE = "ERROR - Client offline";
        public static final String ERROR_FOLLOWER_D_OFFLINE = "ERROR - Follower(d) offline";
        public static final String ERROR_PAYLOAD_NULL = "ERROR - Payload is null";
        public static final String ERROR_SERVERCOMM_NULL = "ERROR - serverComm is null";
        
        public static final String ERROR_MESSAGE_NULL = "ERROR - serverComm.getNextMessage() returns null";
        public static final String ERROR_DUPLICATED_NICKNAME = "";

        private ArrayList<String> errorLog = new ArrayList<String>();
        private int numberOfErrors = 0; 
        @Override
        /**
         * Starts the server which receives and processes messages and keeps track of the clients
         * @author Diana, Ines, Rafael, Ricardo
         * @param serverComm the serverComm through which the server should communicate with clients.
         *
         */
        public void start(ServerComm serverComm) {
                numberOfErrors = 0;
                errorLog.clear();
                this.serverComm = serverComm;
        
                //COMMENT
                boolean stopped = false;
                if(serverComm != null){
                        serverComm.start();
                        while (!stopped) {

                                //      Getting the message, if any
                                ServerSideMessage message = this.serverComm.getNextMessage();
                                if(message != null){
                                        // BIRDSONG_MESSAGE
                                        if (message.getType() == ServerSideMessage.Type.BIRDSONG_MESSAGE) {
                                                try{
                                                        processBirdSongMessage(message);
                                                
                                                } catch (IllegalArgumentException ex){
                                                        if(ex.getMessage().equals(ERROR_CLIENT_OFFLINE)){
                                                                System.out.println(ERROR_CLIENT_OFFLINE);
                                                        } else {
                                                                if(ex.getMessage().equals(ERROR_PAYLOAD_NULL)){
                                                                        serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
                                                                }
                                                        }
                                                        errorLog.add(ex.getMessage());
                                                        numberOfErrors++;
                                                }
                                        }

                                        // CLIENT_CONNECTED
                                        else if (message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED) {
                                                processClientConnectedMessage(message);
                                        }

                                        // CLIENT_DISCONNECTED
                                        else if (message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED) {
                                                try{
                                                        processClientDisconnectedMessage(message);
                                                
                                                } catch (IllegalArgumentException ex){
                                                        if(ex.getMessage().equals(ERROR_CLIENT_OFFLINE)){
                                                                errorLog.add(ERROR_CLIENT_OFFLINE);
                                                                numberOfErrors++;
                                                        }
                                                }

                                        }

                                        // FOLLOW
                                        else if (message.getType() == ServerSideMessage.Type.FOLLOW) {
                                                try{
                                                        processFollowMessage(message);
                                                
                                                } catch (IllegalArgumentException ex){
                                                        if(ex.getMessage().equals(ERROR_CLIENT_OFFLINE)){
                                                                System.out.println(ERROR_CLIENT_OFFLINE);
                                                        } else {
                                                                if(ex.getMessage().equals(ERROR_PAYLOAD_NULL)){
                                                                        serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
                                                                } else {
                                                                        if(ex.getMessage().equals(ERROR_FOLLOWER_D_OFFLINE)){
                                                                                serverComm.sendError(message.getClientNickname(), ERROR_FOLLOWER_D_OFFLINE);
                                                                        }
                                                                }
                                                        }
                                                        errorLog.add(ex.getMessage());
                                                        numberOfErrors++;
                                                }
                                        }

                                        // UNFOLLOW
                                        else if (message.getType() == ServerSideMessage.Type.UNFOLLOW) {
                                                try{
                                                        processUnfollowMessage(message);
                                                
                                                } catch (IllegalArgumentException ex){
                                                        if(ex.getMessage().equals(ERROR_CLIENT_OFFLINE)){
                                                                /* The Network communications service 
                                                                 * shouldn't allow to send a message without a sender.
                                                                 * But the service shouldn't go down because of it
                                                                 */
                                                                System.out.println(ERROR_CLIENT_OFFLINE);
                                                        } else {
                                                                if(ex.getMessage().equals(ERROR_PAYLOAD_NULL)){
                                                                        serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
                                                                } else {
                                                                        if(ex.getMessage().equals(ERROR_FOLLOWER_D_OFFLINE)){
                                                                                serverComm.sendError(message.getClientNickname(), ERROR_FOLLOWER_D_OFFLINE);
                                                                        }
                                                                }
                                                        }
                                                        errorLog.add(ex.getMessage());
                                                        numberOfErrors++;
                                                }
                                        }
                                } else {
                                        errorLog.add(ERROR_MESSAGE_NULL);
                                        numberOfErrors++;
                                        stopped = true;
                                }
                        }
                } else {
                        errorLog.add(ERROR_SERVERCOMM_NULL);
                        numberOfErrors++;
                        throw new IllegalArgumentException(ERROR_SERVERCOMM_NULL);
                }
        }
        /**
         * Updates the relationship between the asker and the followed, that is, asker unfollows the given client
         * @param message The message to be processed (must be from UNFOLLOW type and must have a payload)
         * @throws IllegalArgumentException Throws an exception whenever:
         * The message hasn't a payload;
         * The client isn't online;
         * The desired followed isn't online;
         */
        private void processUnfollowMessage(ServerSideMessage message) throws IllegalArgumentException{
                //      Checking the preconditions
                assert message.getType() == ServerSideMessage.Type.UNFOLLOW;

                // The client is asking to unfollow the user on the payload
                String askerNickname = message.getClientNickname();
                String followedNickname = message.getPayload();
                // Checking if payload has text
                if(message.getPayload() != null){
                        Client asker = getClientByNickname(askerNickname);
                        //      Checking if the asker is online - Precondition
                        if(asker != null){
                                Client followed = getClientByNickname(followedNickname);
                                // Checking if the choosen client to follow is online
                                if(followed != null){
                                        asker.removeFollowed(followed);
                                        followed.removeFollower(asker);                 

                                        // Warn the followed client about what happened;

                                        serverComm.sendNewUnfollow(followedNickname, askerNickname);
                                }
                                else {
                                        throw new IllegalArgumentException(ERROR_FOLLOWER_D_OFFLINE);
                                }
                        }
                        else {
                                throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
                        }
                }
                else {
                        throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
                }
        }
        /**
         * Updates the relationship between the asker and the followed, that is, asker unfollows the given client
         * @param message The message to be processed (must be from FOLLOW type and must have a payload)
         * @throws IllegalArgumentException Throws an exception whenever:
         * The message hasn't a payload;
         * The client isn't online;
         * The desired followed isn't online;
         */
        private void processFollowMessage(ServerSideMessage message) throws IllegalArgumentException{
                // The client is asking to follow the user on the payload
                assert message.getType() == ServerSideMessage.Type.FOLLOW;
                if(message.getPayload() != null){
                        String askerNickname = message.getClientNickname();
                        String followedNickname = message.getPayload();
                        Client asker = getClientByNickname(askerNickname);

                        // Checking if asker is online
                        if(asker != null){
                                Client followed = getClientByNickname(followedNickname);
                                if(followed != null){
                                        asker.addFollowed(followed);
                                        followed.addFollower(asker);

                                        //Warn the followed about what happened;

                                        serverComm.sendNewFollower(followedNickname, askerNickname);
                                }
                                else{
                                        throw new IllegalArgumentException(ERROR_FOLLOWER_D_OFFLINE);
                                }
                        } else {
                                throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
                        }
                } else {
                        throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
                }
        }

        /**
         * Handles the disconnection of a client (sends a message to all users)
         * @param message The message to be processed (must be from CLIENT_DISCONNECTED type)
         * @throws IllegalArgumentException Throws an exception whenever:
         * The client isn't online;
         */
        private void processClientDisconnectedMessage(ServerSideMessage message) throws IllegalArgumentException{
                assert message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED;
                String nickname = message.getClientNickname();

                Client clientGoing = getClientByNickname(nickname);
                //Checking if client is online
                if(clientGoing != null){
                        //Delete client from the online client list
                        onlineClientsList.remove(clientGoing);
                        
                        //Warn the all the users about the disconnection
                        for(Client client : onlineClientsList){
                                String clientNickname = client.getNickname();
                                serverComm.sendClientDisconnected(clientNickname, nickname);
                        }
                        
                        
                } 
                else {
                        throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
                }
        }
        /**
         * Handles the connection of a new client
         * @param message The message to be processed (must be from CLIENT_CONNECTED type)
         */
        private void processClientConnectedMessage(ServerSideMessage message){
                assert message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED;

                String nickname = message.getClientNickname();

                boolean nicknameIsAvailable = checkNicknameAvailability(nickname);

                // Checking if the choosen nickname is available, that is, if it isn't already being used
                if(nicknameIsAvailable){
                        Client justArrived = new Client(nickname);

                        // 1) Warn every one that there is a new client 
                        // 2) And send the list of online users to the new client
                                

                        for(Client client : onlineClientsList){
                                serverComm.sendClientConnected(client.getNickname(), nickname);
                                serverComm.sendClientConnected(nickname, client.getNickname());
                        }
                        
                        // Add the client to the online clients list
                        onlineClientsList.add(justArrived);

                }
                else {
                        serverComm.sendError(nickname, ERROR_DUPLICATED_NICKNAME);
                }
        }

        /**
         * Checks if the nickname is available, that is, if the nickname isn't already being used
         * @param nickname
         * @return
         */
        private boolean checkNicknameAvailability(String nickname) {
                // If there isn't a client with the specified nickname, it's available
                return getClientByNickname(nickname) == null;
        }
        /**
         * Sends the received Birdsong message to all the followers
         * @param message The message to be processed (must be from BIRDSONG_MESSAGE type 
         * and must have a payload)
         * @throws IllegalArgumentException Throws an exception whenever:
         * The message hasn't a payload;
         * The client isn't online;
         */
        private void processBirdSongMessage(ServerSideMessage message) throws IllegalArgumentException{

                assert message.getType() == ServerSideMessage.Type.BIRDSONG_MESSAGE;

                String nickname = message.getClientNickname(); 
                String birdsongMessage = message.getPayload();
                // Check if birdsongMessade has content
                if(birdsongMessage != null){
                        // Get the client

                        Client client = getClientByNickname(nickname);

                        // Checking if client is online

                        if(client != null){
                                // Get the followersList

                                LinkedList<Client> followersList = client.getFollowersList();

                                // Send the message to all the followers

                                for(Client follower : followersList){
                                        String followerNickname = follower.getNickname();
                                        serverComm.sendBirdsongMessage(nickname, followerNickname, birdsongMessage);
                                }
                        }
                        else {
                                throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
                        }
                }
                else{
                        throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
                }
        }


        /**
         * Searches and returns the online client with the given nickname
         * @param nickname
         * @return Null if the client isn't online. Returns the online client with the given nickname
         */

        private Client getClientByNickname(String nickname){            

                Client foundClient = null;
                Iterator<Client> iterator = onlineClientsList.iterator();
                boolean found = false;
                boolean ended = !iterator.hasNext();

                while(!found && !ended){
                        Client client = iterator.next();
                        if(client.getNickname().equals(nickname)){
                                found = true;
                                foundClient = client;
                        } else {
                                if(!iterator.hasNext()){
                                        ended = true;
                                }
                        }
                }

                return foundClient;
        }
        /**
         * Gives access to the Error Log
         * @return errorLog An array of Error description
         */
        public ArrayList<String> getErrorLog(){
                return errorLog;
        }
        
        /**
         * Returns the error count during the last execution
         * @return numberOfErrors the number of errors that occurred during the last execution
         */
        public int getNumberOfErrors(){
                return numberOfErrors;
        }
        
}